#!/usr/bin/env ruby
# frozen_string_literal: true

# Linux codegen for KRF
# Like all code generators, this file is ugly.
# https://filippo.io/linux-syscall-table/

require "yaml"

PLATFORM = `uname -s`.chomp!.downcase!
RELEASE = `uname -r`.chomp!
CONSERVATIVE = (ARGV.shift == "conservative")

abort "Barf: Linux codegen requested but platform is #{PLATFORM}" if PLATFORM != "linux"

HEADER = <<~HEADER
  /* WARNING!
   * This file was generated by KRF's codegen.
   * Do not edit it by hand.
   */
HEADER

SYSCALL_SPECS = Dir[File.join(__dir__, "*.yml")]

SYSCALLS = SYSCALL_SPECS.map do |path|
  spec = YAML.safe_load File.read(path)
  [File.basename(path, ".yml"), spec]
end.to_h

SOURCE_DIR = File.expand_path "../../#{PLATFORM}", __dir__

KERNEL_CONFIG = "/lib/modules/#{RELEASE}/build/.config"

abort "Barf: Missing config for #{RELEASE}: #{KERNEL_CONFIG}" unless File.file? KERNEL_CONFIG

def hai(msg)
  STDERR.puts "[codegen] #{msg}"
end

hai "output directory: #{SOURCE_DIR}"

has_syscall_wrappers = File.readlines(KERNEL_CONFIG).any? do |line|
  line.include? "CONFIG_ARCH_HAS_SYSCALL_WRAPPER=y"
end

gen_files = {
  krf_x: File.open(File.join(SOURCE_DIR, "krf.gen.x"), "w"),
  syscalls_h: File.open(File.join(SOURCE_DIR, "syscalls.gen.h"), "w"),
  syscalls_x: File.open(File.join(SOURCE_DIR, "syscalls.gen.x"), "w"),
  internal_h: File.open(File.join(SOURCE_DIR, "syscalls", "internal.gen.h"), "w"),
}

gen_files.each_value { |file| file.puts HEADER }

SYSCALLS.each do |call, spec|
  # Each syscall requires code generation in 5 files:
  # 1. krf.gen.x, to tell krf that we're interested in faulting it
  # 2. syscalls.gen.h, to prototype the initial wrapper
  # 3. syscalls.gen.x, to set up the initial wrapper
  # 4. syscalls/internal.gen.h, to prototype the internal wrapper
  # 5. syscalls/<syscall>.gen.c, to set up the actual faulty calls

  name = spec["name"] || call
  nr = spec["nr"] || name
  number = "__NR_#{nr}"
  proto, parms = if has_syscall_wrappers
                   ["const struct pt_regs* regs", "regs"]
                 else
                   [spec["proto"], spec["parms"]]
                 end

  hai "#{call} (nr: #{number})"
  gen_files[:krf_x].puts <<~KRF_X
    krf_faultable_table[#{number}] = (void *)&krf_sys_#{call};
  KRF_X

  gen_files[:syscalls_x].puts <<~SYSCALLS_X
    asmlinkage long krf_sys_#{call}(#{proto}) {
      long (*real_#{name})(#{proto}) = (void *)krf_sys_call_table[#{number}];

      if (krf_targeted(KRF_TARGETING_PARMS) && (KRF_RNG_NEXT() % krf_probability) == 0) {
        return krf_sys_internal_#{call}(#{parms});
      } else {
        return real_#{name}(#{parms});
      }
    }
  SYSCALLS_X

  # NOTE(ww): Kernels built with syscall wrappers don't have
  # sys_$whatever exposed via syscalls.h, so typeof doesn't work.
  gen_files[:syscalls_h].puts <<~SYSCALLS_H
    asmlinkage long krf_sys_#{call}(#{proto});
  SYSCALLS_H

  gen_files[:internal_h].puts <<~INTERNAL_H
    long krf_sys_internal_#{call}(#{proto});
  INTERNAL_H

  syscall_c = File.join(SOURCE_DIR, "syscalls", "#{call}.gen.c")
  File.open(syscall_c, "w") do |file|
    file.puts HEADER
    file.puts <<~SETUP
      #include "internal.h"

    SETUP

    fault_table = []
    errors = spec["errors"]
    errors += spec.fetch("unlikely_errors", []) unless CONSERVATIVE
    errors.uniq.each do |fault|
      fault_table << "krf_sys_internal_#{call}_#{fault}"

      file.puts <<~FAULT
        static long krf_sys_internal_#{call}_#{fault}(#{proto}) {
          if (krf_log_faults) {
            KRF_LOG("faulting #{call} with #{fault}\\n");
          }

          return -#{fault};
        }
      FAULT
    end

    file.puts <<~TRAILER
      static long (*fault_table[])(#{proto}) = {
        #{fault_table.join ", "}
      };

      // Fault entrypoint.
      long krf_sys_internal_#{call}(#{proto}) {
        return fault_table[KRF_RNG_NEXT() % NFAULTS](#{parms});
      }
    TRAILER
  end
end

gen_files.each_value(&:close)
